package gateway.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.shaded.json.JSONArray;
import io.netty.buffer.UnpooledByteBufAllocator;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration;
import org.springframework.cloud.gateway.config.GlobalCorsProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static gateway.config.WebSessionIdResolverBeanPostProcessor.HEADER_X_AUTH_TOKEN;
import static org.springframework.security.config.Customizer.withDefaults;

@RequiredArgsConstructor
@Configuration(proxyBeanMethods = false)
class WebSecurityConfig {
    final ObjectMapper objectMapper;
    final ReactiveClientRegistrationRepository clientRegistrationRepository;

    /**
     * @see OAuth2ClientAutoConfiguration
     */
    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.cors(withDefaults()).csrf().disable(); // Fetch/XHR mode login/logout
        http.oauth2Login().authenticationSuccessHandler(authenticationSuccessHandler());
        http.logout().logoutSuccessHandler(oidcLogoutSuccessHandler());
        http.authorizeExchange(authorize -> authorize
                // .pathMatchers("/oauth2/**", "/.well-known/**", "/login", "/logout", "/userinfo").permitAll()
                .pathMatchers("/favicon.ico", "/swagger-ui.html", "/webjars/**", "/v3/api-docs/swagger-config", "/*/v3/api-docs").permitAll()
                .matchers(exchange -> Mono.just(exchange)
                        .map(ServerWebExchange::getRequest)
                        .map(ServerHttpRequest::getHeaders)
                        .filter(h -> h.containsKey(HttpHeaders.AUTHORIZATION))
                        .flatMap($ -> ServerWebExchangeMatcher.MatchResult.match())
                        .switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch())
                ).permitAll() // swagger 调试，放行请求头中含有 Authorization 的请求
                .anyExchange().authenticated()
        );
        // https://docs.spring.io/spring-security/reference/reactive/oauth2/login/advanced.html#webflux-oauth2-login-advanced-login-page
        http.exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec // 返回状态码，而不是重定向到登录页
                .authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED))
                .accessDeniedHandler(new HttpStatusServerAccessDeniedHandler(HttpStatus.FORBIDDEN))
        );
        return http.build();
    }

    // @Bean
    ReactiveOAuth2UserService<OidcUserRequest, OidcUser> reactiveOAuth2UserService() {
        return new OidcReactiveOAuth2UserService() {
            @Override
            public Mono<OidcUser> loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
                return super.loadUser(userRequest).map(oidcUser -> { // 把 IdToken 中的权限信息添加到 OidcUser
                    Set<GrantedAuthority> authorities = new HashSet<>(oidcUser.getAuthorities());
                    ((JSONArray) userRequest.getIdToken().getClaims().get("authorities")).forEach(scope -> authorities.add(new SimpleGrantedAuthority((String) scope)));

                    String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
                    if (StringUtils.hasText(userNameAttributeName)) {
                        return new DefaultOidcUser(authorities, oidcUser.getIdToken(), oidcUser.getUserInfo(), userNameAttributeName);
                    }
                    return new DefaultOidcUser(authorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
                });
            }
        };
    }

    static final String HTML = "<script>window.opener.postMessage('%s', '*');window.close();</script>";

    ServerAuthenticationSuccessHandler authenticationSuccessHandler() {
        return (webFilterExchange, authentication) -> webFilterExchange.getExchange().getSession().map(WebSession::getId).flatMap(sessionId -> {
            try {
                Map<String, String> map = CollectionUtils.newHashMap(2);
                map.put("key", HEADER_X_AUTH_TOKEN);
                map.put("value", sessionId);
                return writeHtml(webFilterExchange.getExchange().getResponse(), String.format(HTML, objectMapper.writeValueAsString(map)));
            } catch (JsonProcessingException e) {
                return Mono.error(new RuntimeException(e));
            }
        });
    }

    static Mono<Void> writeHtml(ServerHttpResponse response, String html) {
        response.getHeaders().setContentType(MediaType.TEXT_HTML);
        return response.writeWith(Flux.create(sink -> {
            NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
            DataBuffer dataBuffer = bufferFactory.wrap(html.getBytes(StandardCharsets.UTF_8));
            sink.next(dataBuffer);
            sink.complete();
        }));
    }

    ServerLogoutSuccessHandler oidcLogoutSuccessHandler() {
        // https://docs.spring.io/spring-security/reference/reactive/oauth2/login/advanced.html#webflux-oauth2-login-advanced-oidc-logout
        OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler = new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository);
        oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
        return oidcLogoutSuccessHandler;
    }

    // @Bean
    CorsWebFilter corsFilter(CorsConfigurationSource source) {
        // https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#cors-configuration
        // https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-cors-webfilter
        return new CorsWebFilter(source);
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource(GlobalCorsProperties globalCorsProperties) {
        // https://docs.spring.io/spring-security/reference/reactive/integrations/cors.html
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        globalCorsProperties.getCorsConfigurations().forEach(source::registerCorsConfiguration);
        return source;
    }
}